/* Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *     http://www.apache.org/licenses/LICENSE-2.0
 *     
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * 
 * Author: Kyle Scholz      http://kylescholz.com/
 * Author: Lorien Henry-Wilkins
 * Copyright: 2006-2007
 */

var VectorModel = function() {
	this.init();
};

var linearGradients = new Object();

VectorModel.prototype = {
	init: function() {
		this.svg_capable = document.implementation.hasFeature("org.w3c.dom.svg", '1.1');
		this.vml_capable = (document.all && !(navigator.userAgent.indexOf("Opera")>=0)) ? true : false;
		
		if ( this.vml_capable ) {
			document.namespaces.add("v","urn:schemas-microsoft-com:vml");
//			document.createStyleSheet().addRule("v\\:*", "behavior:url(#default#VML)");
			document.createStyleSheet().addRule("v\\:*", "behavior:url(#default#VML); position:absolute" );

			var me = this;
			document.createElementNS = function( ns, element ) {
				return me.createElement( element );
			}
		}
	},
	
	createElement: function( element ) {
		
		if ( this.svg_capable ) {
			var svgElement = document.createElementNS("http://www.w3.org/2000/svg", element);
			
			if ( element == "rect" ) {
				svgElement.applyGradient = function( gradient ) {
					if ( gradient.type == "LinearGradient" ) {
						var group = svgElement.parentNode;

						if ( !document.getElementById(gradient.id ) ) {
							var linearGradient = document.createElementNS("http://www.w3.org/2000/svg", "linearGradient");
							linearGradient.id = gradient.id;
							
							// the gradient angle is orthoganal to our base angle
							var angle = (gradient.angle + 90) % 360;
							if ( angle<0 ) { angle = angle%360 + 360; }
							var originAxis = "X";

							var x1Pcnt = 0;
							var y1Pcnt = 0;

							var x2Pcnt = 100;
							var y2Pcnt = 100;
							if ( (angle-45)%180 / 180 > 0 && (angle-45)%180 / 180 < .5 ) {
								originAxis = "Y";
							}

							if ( originAxis=="Y" ) {
								if ( angle > 225 ) {
									// bottom to top
									x2Pcnt = ((angle-225))/90 * 100;
									y2Pcnt = 0;
									x1Pcnt = 100-x2Pcnt; 
									y1Pcnt = 100;
								} else {
									x1Pcnt = ((angle-45))/90 * 100;
									y1Pcnt = 0;
									x2Pcnt = 100 - x1Pcnt;
									y2Pcnt = 100;
								}

							} else {

								if ( angle<45 || angle>=315 ) {
									y2Pcnt = ((angle+225)%90)/90 * 100;
									x2Pcnt = 0;
									y1Pcnt = 100-y2Pcnt;
									x1Pcnt = 100;
								} else {
									y1Pcnt = ((angle+45)%90)/90 * 100;
									x1Pcnt = 0;
									y2Pcnt = 100-y1Pcnt;
									x2Pcnt = 100;
								}
							}

							linearGradient.setAttribute("x1", x1Pcnt + "%");
							linearGradient.setAttribute("y1", y1Pcnt + "%");
							linearGradient.setAttribute("x2", x2Pcnt + "%");
							linearGradient.setAttribute("y2", y2Pcnt + "%");

							var stop1 = document.createElementNS("http://www.w3.org/2000/svg", "stop");
							stop1.setAttribute('offset', "0%");
							stop1.setAttribute('stop-color', gradient.endColor);
							linearGradient.appendChild( stop1 );

							var stop2 = document.createElementNS("http://www.w3.org/2000/svg", "stop");
							stop2.setAttribute('offset', "100%");
							stop2.setAttribute('stop-color', gradient.startColor);
							linearGradient.appendChild( stop2 );

							if ( !group.defs ) {
								group.appendChild( document.createElementNS("http://www.w3.org/2000/svg", "defs") );
							}

							group.getElementsByTagName("defs")[0].appendChild( linearGradient );
							this.setAttribute("fill", "url(#" + gradient.id + ")");
							//this.setAttribute("stroke-width","1");
						}
					}
				}
			}
			
			return svgElement;		

		} else if ( this.vml_capable ) {
			if ( element == "svg" ) {
				return createVMLSurface();
			} else if ( element == "g" ) {
				return createVMLGroup();
			} else if ( element == "circle") {
				return createVMLCircle();
			} else if ( element == "ellipse") {
				return createVMLEllipse();
			} else if ( element == "rect") {
				return createVMLRectangle();
			} else if ( element == "line") {
				return createVMLLine();
			} else if (element == "linearGradient") {
				return createVMLLinearGradient();
			} else if( element == "stop" ) {
				return createVMLStop();
			} else if( element == "defs" ) {
				return createVMLDefs();
			}

		} else {
			// throw an exception? do HTML?
		}
	}
}

var createVMLSurface = function() {
//	var domElement = document.createElement("v:group");	
	var domElement = document.createElement("div");
	domElement.style.position = "absolute";
	domElement.style.left = "0px";
	domElement.style.top = "0px";

	domElement.setAttribute = function( key, value ) {
		if ( key == "width" ) {
			this.style.width = value;
		} else if ( key == "height" ) {
			this.style.height = value;
		} else if ( key == "x" ) {
			this.style.left = value;
		} else if ( key == "y" ) {
			this.style.top = value;
		}
	}
	
	return domElement;
};

var createVMLGroup = function() {
//	var domElement = document.createElement("v:group");	
	var domElement = document.createElement("div");
	domElement.style.position = "absolute";
	domElement.style.left = "0px";
	domElement.style.top = "0px";

	domElement.defs = {};

	domElement.setAttribute = function( key, value ) {
		if ( key == "width" ) {
			this.style.width = value;
		} else if ( key == "height" ) {
			this.style.height = value;
		} else if ( key == "x" ) {
			this.style.left = value;
		} else if ( key == "y" ) {
			this.style.top = value;
		} else if ( key == "transform" ) {
			var reg = /scale\s*\(\s*(\d+\.\d+)\s*(\d+\.\d+)\s*\)/i;
			var ar = reg.exec(value);
			var xScale = 1;
			var yScale = 1;
			if( ar ){
				xScale = ar[1];
				yScale = ar[2];
			}
			for( var i=0; i<this.childNodes.length; i++ ){
				var child = this.childNodes[i];
				var width = parseInt(child.style.width);
				var height = parseInt(child.style.height);
				var left = parseInt(child.style.left);
				var top = parseInt(child.style.top);
				child.style.width = width * xScale + 'px';
				child.style.height = height * yScale + 'px';
				child.style.left = left * xScale + 'px';
				child.style.top = top * yScale + 'px';
			}
		}
	}
	
	return domElement;
};

var createVMLCircle = function() {
	var domElement = document.createElement("v:oval");
	domElement.leftPos = 0;
	domElement.topPos = 0;
	domElement.rVal = 0;
	
	domElement.setAttribute = function( key, value ) {

		if ( key == "cx" ) {
			if( this.rVal == 0 ) {
				this.leftPos = parseInt(value);
			} else {
				this.leftPos = parseInt(value) - parseInt(this.rVal) + 'px';
				this.style.left = this.leftPos;
			}
		} else if ( key == "cy" ) {
			if( this.rVal == 0 ) {
				this.topPos = parseInt(value);
			} else {
				this.topPos = parseInt(value) - parseInt(this.rVal) + 'px';
				this.style.top = this.topPos;
			}
		} else if ( key == "stroke" ) {
			this.strokecolor = value;
		} else if ( key == "stroke-width" ) {
			this.strokeweight = parseFloat(value)/1.2 + "pt";
		} else if ( key == "fill" ) {
			this.fillcolor = value;
		} else if ( key == "r" ) {
			//save the last width
			this.rVal = parseInt(value);
			var last = (this.style.width) ? parseInt( this.style.width ) : 0;
			
			//set width and height to r*2
			this.style.width = this.rVal*2 + 'px';
			this.style.height = this.rVal*2 + 'px';
			
			//get left and top attributes
			//var left = (this.style.left) ? parseInt( this.style.left ) : 0; 
			//var top = (this.style.top) ? parseInt( this.style.top ) : 0;
			
			//set new left pos
			//this.style.left = left + parseInt( last )/2 - parseInt( value ) /2;
			//this.style.top = top + parseInt( last )/2 - parseInt( value ) /2;
			//this.style.left = this.left + parseInt( last )/2 - parseInt( value ) /2;
			//this.style.top = this.top + parseInt( last )/2 - parseInt( value ) /2;
			if( last == 0 ) {
				this.leftPos = this.leftPos - this.rVal;
				this.topPos = this.topPos - this.rVal;
				this.style.left = this.leftPos + 'px';
				this.style.top = this.topPos + 'px';
			} else {
				this.style.left = this.leftPos + parseInt(last)/2 - this.rVal;
				this.style.top = this.topPos + parseInt(last)/2 - this.rVal;
			}
		}
	}

	domElement.getAttribute = function( key ) {

		if ( key == "cx" ) {
			var x = (parseInt(this.style.left) + parseInt(this.style.width)/2) ? 
				(parseInt(this.style.left) + parseInt(this.style.width)/2) + 'px' : 0 + 'px'; 
			return x;
		} else if ( key == "cy" ) {
			var y = (parseInt(this.style.top) + parseInt(this.style.height)/2) ? 
				(parseInt(this.style.top) + parseInt(this.style.height)/2) + 'px' : 0 + 'px'; 
			return y;
		} else if ( key == "stroke" ) {
			return this.strokecolor;
		} else if ( key == "stroke-width" ) {
			return parseFloat( this.strokecolor ) * 1.2 + 'px';
		} else if ( key == "fill" ) {
			return this.fillcolor;
		} else if ( key == "r" ) {
			return parseInt( this.style.width )/2;
		}
	}
	
	return domElement;
};

var createVMLEllipse = function() {
	var domElement = document.createElement("v:oval");
	domElement.leftPos = 0;
	domElement.topPos = 0;
	domElement.rxVal = 0;
	domElement.ryVal = 0;
	domElement.setAttribute = function( key, value ) {

		if ( key == "cx" ) {
			if( this.rxVal == 0 ) {
				this.leftPos = parseInt(value);
			} else {
				this.leftPos = parseInt(value) - parseInt(this.rxVal) + 'px';
				this.style.left = this.leftPos;
			}
		} else if ( key == "cy" ) {
			if( this.ryVal == 0 ) {
				this.topPos = parseInt(value);
			} else {
				this.topPos = parseInt(value) - parseInt(this.ryVal) + 'px';
				this.style.top = this.topPos;
			}
		} else if ( key == "stroke" ) {
			this.strokecolor = value;
		} else if ( key == "stroke-width" ) {
			this.strokeweight = parseFloat(value)/1.2 + "pt";
		} else if ( key == "fill" ) {
			this.fillcolor = value;
		} else if ( key == "rx" ) {
			this.rxVal = parseInt(value);
			var last = (this.style.width) ? parseInt( this.style.width ) : 0;
			this.style.width = this.rxVal*2 + 'px'; 
			
			//var left = (this.style.left) ? parseInt( this.style.left ) : 0;
			
			if( last == 0 ) {
				this.leftPos = this.leftPos - this.rxVal;
				this.style.left = this.leftPos + 'px';
			} else { 
				this.style.left = this.leftPos + parseInt(last)/2 - this.rxVal;
			}
		} else if ( key == "ry" ) {
			this.ryVal = parseInt(value);
			var last = (this.style.height) ? parseInt( this.style.height ) : 0; 
			this.style.height = this.ryVal*2 + 'px';
			
			//var top = (this.style.top) ? parseInt( this.style.top ) : 0;
			
			if( last == 0 ) {
				this.topPos = this.topPos - this.ryVal;
				this.style.top = this.topPos + 'px';
			} else {
				this.style.top = this.topPos + parseInt(last)/2 - this.ryVal;
			}
		}
	}

	domElement.getAttribute = function( key ) {

		if ( key == "cx" ) {
			var x = (parseInt(this.style.left) + parseInt(this.style.width)/2) ? 
				(parseInt(this.style.left) + parseInt(this.style.width)/2) + 'px' : 0 + 'px'; 
			return x;
		} else if ( key == "cy" ) {
			var y = (parseInt(this.style.top) + parseInt(this.style.height)/2) ? 
				(parseInt(this.style.top) + parseInt(this.style.height)/2) + 'px' : 0 + 'px'; 
			return y;
		} else if ( key == "stroke" ) {
			return this.strokecolor;
		} else if ( key == "stroke-width" ) {
			return parseFloat( this.strokecolor ) * 1.2 + 'px';
		} else if ( key == "fill" ) {
			return this.fillcolor;
		} else if ( key == "rx" ) {
			return parseInt( this.style.width )/2;
		} else if ( key == "ry" ) {
			return parseInt( this.style.height )/2;
		}
	}
	
	return domElement;
};

var LinearGradient = function( id, startColor, endColor, angle ) {
	this.init( id, startColor, endColor, angle );
};
LinearGradient.prototype = {
	init: function( id, startColor, endColor, angle ) {
		this.id = id;
		this.type = "LinearGradient";
		this.startColor = startColor;
		this.endColor = endColor;
		this.angle = angle;
	}
}


var createVMLRectangle = function() {
	var domElement = document.createElement("v:roundrect");
	
	domElement.rxy = 0;
	
	domElement.applyGradient = function( gradient ) {
		if ( gradient.type == "LinearGradient" ) {
			this.fillcolor = gradient.startColor;
			var fill = document.createElement( "v:fill" );
			fill.type = "gradient";
			fill.color2 = gradient.endColor;
			fill.angle = gradient.angle;
			this.appendChild(fill);
		}
	}
	
	domElement.applyLinGradient = function( linGradient ) {
		var stop1 = linGradient.stops[0];
		var stop2 = linGradient.stops[linGradient.stops.length-1];
		this.fillcolor = stop1.stopColor;
		var fill = document.createElement( "v:fill" );
		fill.type = "gradient";
		fill.color2 = stop2.stopColor;
		
		if( linGradient.y1 == 0 ) {
			var rads = Math.atan((linGradient.x2-linGradient.x1)/linGradient.y2);
			var degs = rads * (180/Math.PI);
			fill.angle = (degs + 180) % 360;
		} else if( linGradient.y1 > 0 ) {
			var rads = Math.atan((linGradient.y2-linGradient.y1)/linGradient.x2);
			var degs = rads * (180/Math.PI);
			fill.angle = (degs + 180) % 360;
		}
		this.appendChild(fill);
	}

	domElement.setAttribute = function( key, value ) {

		if ( key == "x" ) {
			this.style.left = parseInt(value) + 'px';
			return;
		} else if ( key == "y" ) {
			this.style.top = parseInt(value) + 'px';
			return;
		} else if ( key == "stroke" ) {
			this.strokecolor = value;
			return;
		} else if ( key == "stroke-width" ) {
			this.strokeweight = parseFloat(value)/1.2 + "pt";
			return;
		} else if ( key == "fill" ) {
			if( value.substring(0,3) == "url" ) {
				//get id
				var gradId = value.substring(5,value.length-1);
				this.applyLinGradient(linearGradients[gradId]);
			} else {
				this.fillcolor = value;
			}
			return;
		} else if ( key == "width" || key == "height" || key == "rx" || key == "ry") {
			if( key == "width" ) {
				this.style.width = parseInt(value) + 'px';
			} else if ( key == "height" ) {
				this.style.height = parseInt(value) + 'px';
			} else if ( key == "rx" || key == "ry" ) {
				if( parseInt(value) ){
					this.rxy = parseInt(value);
				}
			}
			/* compute the arc ratio */
			if( this.rxy != 0  ){
				var dim = parseInt(this.style.width);
				var dimy = parseInt(this.style.height);
				if( !dim ){
					dim = 0;
				}
				if( !dimy ){
					dimy = 0;
				}
				if( dimy < dim ){
					dim = dimy;
				}
				var ratio = this.rxy/dim;
				this.arcsize = ratio;
			} else {
				this.arcsize = 0;
			}
			return;
		}
	}

	domElement.getAttribute = function( key ) {

		if ( key == "x" ) {
			var x = parseInt(this.style.left) ?	parseInt(this.style.left) + 'px' : 0 + 'px'; 
			return x;
		} else if ( key == "y" ) {
			var y = parseInt(this.style.top) ?	parseInt(this.style.top) + 'px' : 0 + 'px'; 
			return y;
		} else if ( key == "stroke" ) {
			return this.strokecolor;
		} else if ( key == "stroke-width" ) {
			return parseFloat( this.strokecolor ) * 1.2 + 'px';
		} else if ( key == "fill" ) {
			return this.fillcolor;
		} else if ( key == "width" ) {
			return parseInt( this.style.width );
		} else if ( key == "height" ) {
			return parseInt( this.style.height );
		}
	}
	
	return domElement;
};

var createVMLLine = function() {
	var domElement = document.createElement("v:line");
	
	domElement.setAttribute = function( key, value ) {

		if ( key == "x1" ) {
			this.x1 = value;
			this.y1 = this.y1 ? this.y1 : 0;
			this.from = this.x1 + ' ' + this.y1;
			return;
		} else if ( key == "y1" ) {
			this.y1 = value;
			this.x1 = this.x1 ? this.x1 : 0;
			this.from = this.x1 + ' ' + this.y1;
			return;
		} else if ( key == "x2" ) {
			this.x2 = value;
			this.y2 = this.y2 ? this.y2 : 0;
			this.to = this.x2 + ' ' + this.y2;
			return;
		} else if ( key == "y2" ) {
			this.y2 = value;
			this.x2 = this.x2 ? this.x2 : 0;
			this.to = this.x2 + ' ' + this.y2;
			return;
		} else if ( key == "stroke" ) {
			this.strokecolor = value;
			return;
		} else if ( key == "stroke-width" ) {
			this.strokeweight = parseFloat(value)/1.2 + "pt";
			return;
		} else if ( key == "fill" ) {
			this.fillcolor = value;
			return;
		}
	}

	domElement.getAttribute = function( key ) {

		if ( key == "x1" ) {
			if ( !this.x1 ) { return 0; }
			return x1;
		} else if ( key == "y1" ) {
			if ( !this.y1 ) { return 0; }
			return y1;
		} else if ( key == "x2" ) {
			if ( !this.x2 ) { return 0; }
			return x2;
		} else if ( key == "y2" ) {
			if ( !this.y2 ) { return 0; }
			return y2;
		} else if ( key == "stroke" ) {
			return this.strokecolor;
		} else if ( key == "stroke-width" ) {
			return parseFloat( this.strokecolor ) * 1.2 + 'px';
		} else if ( key == "fill" ) {
			return this.fillcolor;
		} else if ( key == "width" ) {
			return parseInt( this.style.width );
		} else if ( key == "height" ) {
			return parseInt( this.style.height );
		}
	}
	
	return domElement;
};

var createVMLPath = function() {

	var domElement = document.createElement("v:shape");

	domElement.style.width = '21600';
	domElement.style.height = '21600';
	domElement.coordsize = '21600, 21600';
	domElement.style.position = "absolute";

	domElement.setAttribute = function( key, value ) {

		if ( key == "d" ) {

			// --- SVG ---
			// args: http://www.w3schools.com/svg/svg_path.asp 

			// --- VML ---
			// args: m l x e v?

			var cursorX = 0;
			var cursorY = 0;

			value = value.replace(/(\d*)((\.*\d*)(e ?-?\d*))/g,"$1");
			var pathCommands = value.match( /([MLHVCSQTAZ].*?)(?=[MLHVCSQTAZ]|$)/gi );

			var newPath = "";

			for( var i=0; i<pathCommands.length; i++ ) {
				var command = pathCommands[i].substring(0,1);
				var params = pathCommands[i].substring(1,pathCommands[i].length);
				switch( command ) {
					case "M": // moveTo absolute
						var coords = params.split(",");
						cursorX = parseInt(coords[0]);
						cursorY = parseInt(coords[1]);
						break;

					case "A": // arc absolute:
					// SVG: rx ry x-axis-rotation large-arc-flag sweep-flag x y
					// VML: center (x,y) size(w,h) start-angle, end-angle

var command = "ae";
						var args = params.split(/[, ]+/);
						args[0] = parseInt(args[0]); 
						args[1] = parseInt(args[1]);

						args[2] = parseInt(args[2]); 
						args[3] = parseInt(args[3]);

						args[4] = parseInt(args[4]); 
						args[5] = parseInt(args[5]);
//params = "2250 1590 791 791 -3538944 9437184";
						params = args[4] + " " + args[5] + " " + args[2]*2 + " " + args[3]*2 + " 0 360";

//alert( command + params );
						break;

					case "l": // lineTo relative
						var coords = params.split(",");
						coords[0] = parseInt(coords[0]) + parseInt(cursorX); 
						coords[1] = parseInt(coords[1]) + parseInt(cursorY);
						cursorX = parseInt(coords[0]);
						cursorY = parseInt(coords[1]);
						params = coords[0] + "," + coords[1] + " ";
						break;

					default:
				}
				newPath += command + params;					
			}

			this.path = newPath;
			return;
		} else if ( key == "stroke" ) {
			this.strokecolor = value;
			return;
		} else if ( key == "stroke-width" ) {
			this.strokeweight = parseFloat(value)/1.2 + "pt";
			return;
		} else if ( key == "fill" ) {
			this.fillcolor = value;
			return;
		}
	}

	domElement.getAttribute = function( key ) {
		alert("not implemented");
	}
	
	return domElement;
};

var createVMLLinearGradient = function(){
	return new LinGradient();
};

var createVMLStop = function() {
	var myStop = new Object();
	
	myStop.setAttribute = function( key, value ) {
		if( key == 'offset' ) {
			this.offset = value;
		} else if( key == 'stop-color' ) {
			this.stopColor = value;
		}
	}
	
	return myStop;
};

var createVMLDefs = function() {
	var myDef = new Object();
	myDef = document.createElement("defs");
	myDef.appendChild = function(child){};
	return myDef;
}

var LinGradient = function( ) {
	this.init( );
};
LinGradient.prototype = {
	init: function( ) {
		this.stops = new Array();
	},
	
	setAttribute: function( key, value ) {
		if( key == "id" ){
			this.id = value;
			linearGradients[this.id] = this;
		} else if( key == "x1" ) {
			this.x1 = value.substring(0,value.length-1);
		} else if( key == "y1" ) {
			this.y1 = value.substring(0,value.length-1);
		} else if( key == "x2" ) {
			this.x2 = value.substring(0,value.length-1);
		} else if( key == "y2" ) {
			this.y2 = value.substring(0,value.length-1);
		}
	},
	
	appendChild: function( stop ) {
		this.stops.push(stop);
	}
}

/* Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *     http://www.apache.org/licenses/LICENSE-2.0
 *     
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * 
 * Author: Kyle Scholz      http://kylescholz.com/
 * Author: Lorien Henry-Wilkins
 * Copyright: 2006-2007
 */

/**
 * ParticleModel
 * 
 * @author Kyle Scholz
 * @author Lorien Henry-Wilkins
 * 
 * @version 0.3
 * 
 * The ParticleModel drives our graph. It applies the forces that dictate the
 * position of nodes.
 * 
 * This implementation is optimized to:
 * - Draw nodes only when visible changes have occured.
 * - Recompute forces on older particles at lower frequency
 *   - Can be further optimized to adjust frequency based on a force's volatility
 *     or the volatility in it's constituent particles' positions.
 */
var ParticleModel = function( view ){
	this.init( view );
}
ParticleModel.prototype = {

	/*
	 * Initialize the ParticleModel
	 */
	init: function( view ) {
		this.ENTROPY_THROTTLE=true;

		this.view = view;

		this.particles = new Array();

		this.nextParticleId = 0;

		this.springs = new Array();

		this.activeSprings = new Array();

		this.springLast = 0;

		this.magnets = new Array();

		this.activeMagnets = new Array();

		this.magLast = 0;

		this.integrator = new RungeKuttaIntegrator( this, view );

		this.timer = new Timer( 1 );
		
		this.timer.subscribe( this );	

		this.setSize(this.view.frameWidth,this.view.frameHeight,this.view.skewX,this.view.skewY);
	},
	
	/*
	 * Perform a timestep.
	 */
	tick: function() {
		this.integrator.step(1);
		return this.draw();
	},

	/*
	 * Set boundaries.
	 * 
	 * @param {Object} frameWidth
	 * @param {Object} frameHeight
	 */
	setSize: function( frameWidth, frameHeight ) {
		this.boundsLeft = (-frameWidth/this.view.skewX)/2;
		this.boundsRight = (frameWidth/this.view.skewX)/2;
		this.boundsTop = (-frameHeight/this.view.skewY)/2;
		this.boundsBottom = (frameHeight/this.view.skewY)/2;
	},

	/*
	 * Draw all particles
	 */
	draw: function( force ) {
		var view = this.view;
		var particles = this.particles;
		var moved = 0;
		
		var skewX = this.view.skewX;
		var skewY = this.view.skewY;		
		for ( var i=0, l=particles.length; i<l; i++ ) {
			var particle = particles[i];

			//bounds checking		
			if( this.boundsLeft ) { //only check if the bounds have been set
				if ( particle.positionX < this.boundsLeft+(particle.width/2)/skewX ) {
					particle.positionX = this.boundsLeft+(particle.width/2)/skewX;
				} else if ( particle.positionX > (this.boundsRight-(particle.width/2)/skewX) ) {
					particle.positionX = this.boundsRight-(particle.width/2)/skewX;
				}
	
				if ( particle.positionY < this.boundsTop+(particle.height/2/skewY) ) {
					particle.positionY = this.boundsTop+(particle.height/2/skewY);
				} else if ( particle.positionY > (this.boundsBottom-(particle.height/2/skewY)) ) {
					particle.positionY = this.boundsBottom-(particle.height/2/skewY);
				}
			}

			var newDrawPositionX = Math.round(particle.positionX*2)/2;
			var newDrawPositionY = Math.round(particle.positionY*2)/2;
			// only redraw if particle position has changed by 2px
			if ( newDrawPositionX != particle.lastDrawPositionX || newDrawPositionY != particle.lastDrawPositionY || force ) {
				view.drawNode( particle );					
				moved++;
			}
			particle.lastDrawPositionX = newDrawPositionX;
			particle.lastDrawPositionY = newDrawPositionY;
		}
		return moved;
	},
	
	/*
	 * Create and add a new particle to the system.
	 *  
	 * @param {Number} mass
	 * @param {Number} x
	 * @param {Number} y
	 */
	makeParticle: function( mass, x, y ) {
		var particle = new Particle( mass, x, y );
		particle.id = this.nextParticleId++;
		this.particles.push( particle );
		this['integrator'].allocateParticle( particle.id );

		if ( this.timer.interupt ) {
			this.timer.start();
		}

		return particle;
	},

	/*
	 * Create a Spring between 2 nodes
	 * 
	 * @param {Particle} a  - A Particle.
	 * @param {Particle} b  - The other Partilce.
	 * @param {Number} springConstant - The Spring constant.
	 * @param {Number} dampingConstant  - The damping constant.
	 * @param {Number} restLength  - The length of the Spring at rest.
	 */	
	makeSpring: function( a,  b, springConstant, dampingConstant, restLength ) {
		var spring = new Spring(a, b, springConstant, dampingConstant, restLength);
		this.springs.push( spring );
		this.activeSprings.push( spring );
		return( spring );
	},
	
	/*
	 * Create a magnetic force between nodes
	 * 
	 * @param {Particle} a  - A Particle.
	 * @param {Particle} b  - The other Particle.
	 * @param {Number} g - A gravitational constant (that's right)
	 * @param {Number} distanceMin
	 */
	makeMagnet: function( a, b, g, distanceMin ) {
		var magnet = new Magnet( a, b, g, distanceMin );
		this.magnets.push( magnet );
		this.activeMagnets.push( magnet );
		if ( this.activeMagnets.length > 50 ) {
			this.activeMagnets.shift();
		}
		return magnet;		
	},

	/*
	 * Calculate and aggregate all forces for each particle
	 */
	applyForces: function() {

		/* Spring Forces */

		var activeSprings = this.activeSprings;
		var springs = this.springs;
		var springLast = this.springLast;

		var scanLength=parseInt(springs.length/10);

		// Active Springs
		for( var i=0, l=activeSprings.length; i<l; i++ ) {
			activeSprings[i].apply();
			activeSprings[i].age++;
		}

// todo: I'm only pulling one node from the active collection per iteration		
		var springLen = this.activeSprings.length;
		if ( springLen > 0 && this.activeSprings[0].age > 20 ) {
			this.activeSprings.shift();
		}

		// Calculate forces from Springs in window
		for( var i=springLast, t=springLast+scanLength, l=springs.length; i<t && i<l; i++ ) {
			springs[i].apply();
		}

		// Shift Window
		this['springLast']+=scanLength;
		if ( this['springLast'] >= springs.length ) {
			this['springLast'] = 0;
		}

		/* Magnetic Forces */

		var activeMagnets = this.activeMagnets;
		var magnets = this.magnets;
		var magLast = this['magLast']

		scanLength=parseInt(magnets.length/10);

		// Active Magnets
		for( var i=0, l=activeMagnets.length; i<l; i++ ) {
			activeMagnets[i].apply();
			activeMagnets[i].age++;
		}

// todo: I'm only pulling one node from the active collection per iteration
		var magLen = this.activeMagnets.length;
		if ( magLen > 0 && this.activeMagnets[0].age > 50 ) {
			this.activeMagnets.shift();
		}

		// Calculate forces from Magnets in window
		for( var i=magLast, t=magLast+scanLength, l=magnets.length; i<t && i<l; i++ ) {
			magnets[i].apply();
		}

		// Shift Window
		this['magLast']+=scanLength;
		if ( this['magLast'] >= magnets.length ) {
			this['magLast'] = 0;
		}
	},

	/*
	 * Reset all of the stored forces in the model. 
	 */	
	reset: function() {

		var springs = this.springs;
		for( var i=0, l=springs.length; i<l; i++ ) {
			springs[i].forceX = 0;
			springs[i].forceY = 0;
		}

		var magnets = this.magnets;
		for( var i=0, l=magnets.length; i<l; i++ ) {
			magnets[i].forceX = 0;
			magnets[i].forceY = 0;
		}

		var particles = this.particles;
		for( var i=0, l=particles.length; i<l; i++ ) {
			particles[i].forceX = 0;
			particles[i].forceY = 0;
		}

		var particles = this.particles;
		for( var i=0, l=particles.length; i<l; i++ ) {
			this.integrator.allocateParticle(i);
		}

		if ( this.timer.interupt ) {
			this.timer.start();
		}
	},

	/*
	 * Call animation timer update. Instruct the timer slow down if the
	 * graph is settling.
	 */
	update: function() {
		var moved = this.tick();
		var result = 1;
		if ( this.ENTROPY_THROTTLE && this.particles.length > 2 ) {
			var e = (moved/(this.particles.length));
			if ( e < .01 ) {
				this.stop();
			} else if ( e < .05 ) {
				return(1000);
			} else if ( e < .1 ) {
			return(200);
			}
			return(1);
		}
		return result;
	},
	
	/*
	 * Start animation timer.
	 */
	start: function() {
		this.timer.start();
	},

	/*
	 * Stop animation timer.
	 */	
	stop: function() {
		this.timer.stop();
	},

	/*
	 * Clear particles and forces. Wipe out intermediate data from view and integrator. 
	 */	
	clear: function() {
		this.particles = new Array();

		this.nextParticleId = 0;

		this.springs = new Array();

		this.activeSprings = new Array();

		this.springLast = 0;

		this.magnets = new Array();

		this.activeMagnets = new Array();

		this.magLast = 0;
		
		this.view.clear();
		
		this.integrator.initialize( this, this.view );
	}
}
/* Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *     http://www.apache.org/licenses/LICENSE-2.0
 *     
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * 
 * Author: Kyle Scholz      http://kylescholz.com/
 * Copyright: 2006-2007
 */

/**
 * Magnet
 * 
 * @author Kyle Scholz
 * 
 * @version 0.3
 * 
 * A magnetic force (attractive(+) or repulsive(-)) between two particles.
 * 
 * @param {Particle} a  - A Particle.
 * @param {Particle} b  - The other Particle.
 * @param {Number} g - A gravitational constant (that's right)
 * @param {Number} distanceMin
 */
var Magnet = function( a, b, g, distanceMin ) {
	this.init( a, b, g, distanceMin );
}
Magnet.prototype = {

	/*
	 * Initialize the Magnet Force.
	 *  
	 * @param {Particle} a  - A Particle.
	 * @param {Particle} b  - The other Particle.
	 * @param {Number} g - A gravitational constant (that's right)
	 * @param {Number} distanceMin
	 */
	init: function( a, b, g, distanceMin ) {
		this['a'] = a;
		
		this['b'] = b;
		
		this['g'] = g;
		
		this['distanceMin'] = distanceMin;
		
		this['age'] = 0;
		
		this['forceX'] = 0;

		this['forceY'] = 0;
	},
	
	/*
	 * Apply an attractive or repulsive force based on distance between particles
	 */
	apply: function() {

		// Determine the distance between particles
		var dx = this.a.positionX - this.b.positionX;
		var dy = this.a.positionY - this.b.positionY;
		var distance = Math.sqrt( dx*dx	+ dy*dy );

		if ( distance == 0 ) {
			return;
		} else {
			dx *= (1/distance);
			dy *= (1/distance);
		}

		// Determine the magnetic force.
		var force = this.g * this.a.mass * this.b.mass;
		if (distance < this.distanceMin) {
			force *= 1/(this.distanceMin * this.distanceMin );
		} else {
			force *= 1/(distance * distance);
		}

		// Apply force to vectors
		dx *= force;
		dy *= force;

		// Get the difference since last application 
		var dfx = dx - this.forceX;
		var dfy = dy - this.forceY;

		if (!this.a.fixed && !this.a.selected) {
			this.a.forceX -= dfx;
			this.a.forceY -= dfy;
		}
		
		if (!this.b.fixed && !this.b.selected) {
			this.b.forceX += dfx;
			this.b.forceY += dfy;
		}

		// Update the spring force		
		this.forceX = dx;
		this.forceY = dy;
	}
}

/* Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *     http://www.apache.org/licenses/LICENSE-2.0
 *     
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * 
 * Author: Kyle Scholz      http://kylescholz.com/
 * Copyright: 2006-2007
 */

/**
 * Spring
 * 
 * @author Kyle Scholz
 * 
 * @version 0.3
 * 
 * A Spring Force between two Particle.
 * 
 * @param {Particle} a  - A Particle.
 * @param {Particle} b  - The other Partilce.
 * @param {Number} springConstant - The Spring constant.
 * @param {Number} dampingConstant  - The damping constant.
 * @param {Number} r  - The rest length of the Spring.
 */
var Spring = function( a, b, springConstant, dampingConstant, restLength ) {
	this.init( a, b, springConstant, dampingConstant, restLength );
}
Spring.prototype = {

	/*
	 * Initialize the Spring Force.
	 *  
	 * @param {Particle} a  - A Particle.
	 * @param {Particle} b  - The other Partilce.
	 * @param {Number} springConstant - The Spring constant.
	 * @param {Number} dampingConstant  - The damping constant.
	 * @param {Number} restLength  - The length of the Spring at rest.
	 */
	init: function( a, b, springConstant, dampingConstant, restLength ) {
		this['springConstant'] = springConstant;
		
		this['damping'] = dampingConstant;
		
		this['restLength'] = restLength;
		
		this['a'] = a;
		
		this['b'] = b;

		// The force exerted by the Spring on the X axis.
		this.forceX = 0;

		// The force exerted by the Spring on the Y axis.
		this.forceY = 0;
	},
	
	/*
	 * Apply a spring force based on distance between particles.
	 */
	apply: function() {

		// Determine the current length of the spring
		var dx = this.a.positionX - this.b.positionX;
		var dy = this.a.positionY - this.b.positionY;
		var springLength = Math.sqrt( dx*dx + dy*dy );

		if ( springLength == 0 ) {
			dx = 0;
			dy = 0;
		} else {
			dx *= (1/springLength);
			dy *= (1/springLength);
		}

		// Determine the spring force
		var springForce = -(springLength - this.restLength) * this.springConstant;

		// Determine the damping force
		var vx = this.a.velocityX - this.b.velocityX;
		var vy = this.a.velocityY - this.b.velocityY;
		var dampingForce = -this.damping * (dx * vx + dy * vy);

		// Determine the sum force
		var force = springForce + dampingForce;

		// Apply force to vectors
		dx *= force;
		dy *= force;

		// Get the difference since last application 
		var dfx = dx - this.forceX;
		var dfy = dy - this.forceY;
		
		if (!this.a.fixed && !this.a.selected) {
			this.a.forceX += dfx;
			this.a.forceY += dfy;
		}

		if (!this.b.fixed && !this.b.selected) {
			this.b.forceX -= dfx;
			this.b.forceY -= dfy;
		}

		// Update the spring force
		this.forceX = dx;
		this.forceY = dy;
	}
}

/* Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *     http://www.apache.org/licenses/LICENSE-2.0
 *     
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * 
 * Author: Kyle Scholz      http://kylescholz.com/
 * Copyright: 2006-2007
 */

/**
 * Particle
 * 
 * @author Kyle Scholz
 * 
 * @version 0.3
 * 
 * A Particle in our model.
 * 
 * @param {Number} mass
 * @param {Number} positionX
 * @param {Number} positionY
 */
var Particle = function( mass, positionX, positionY ){
	this.init( mass, positionX, positionY );
}
Particle.prototype = {
	
	/*
	 * Initialize
	 * 
	 * @param {Object} mass
	 * @param {Object} position
	 */
	init: function( mass, positionX, positionY ) {

		this['positionX'] = positionX;
		this['positionY'] = positionY;

		this['originalPositionX'] = positionX;
		this['originalPositionY'] = positionY;

		this['lastDrawPositionX'] = 0;
		this['lastDrawPositionY'] = 0;

		this['mass'] = mass;
	
		this['forceX'] = 0;
		this['forceY'] = 0;

		this['velocityX'] = 0;
		this['velocityY'] = 0;

		this['originalVelocityX'] = 0;
		this['originalVelocityY'] = 0;

		this['fixed'] = false;

		this['selected'] = false;
		
		this['age'] = 0;
		
		// we use width and height for bounds checking	
		this['width'] = 0;	

		this['height'] = 0;	
	}
}

/* Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *     http://www.apache.org/licenses/LICENSE-2.0
 *     
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * 
 * Author: Kyle Scholz      http://kylescholz.com/
 * Author: Lorien Henry-Wilkins
 * Copyright: 2006-2007
 */

/**
 * RungeKuttaIntegrator
 * 
 * @author Kyle Scholz
 * @author Lorien Henry-Wilkins
 * 
 * @version 0.3
 * 
 * A JavaScript implementation of the Runge-Kutta integrator. This implementation
 * is optimized to reduce the number of symbol lookups.
 * 
 * @see Inspired by traer.physics: http://www.cs.princeton.edu/~traer/physics/
 * @see Algorithm: http://calculuslab.deltacollege.edu/ODE/7-C-3/7-C-3-h.html
 * 
 * @param {ParticleModel} particleModel
 */
var RungeKuttaIntegrator = function( particleModel, view ) {
	this.initialize( particleModel, view );
};
RungeKuttaIntegrator.prototype = {

	/*
	 * @param {ParticleModel} particleModel
	 * @param {View} view
	 */
	initialize: function( particleModel, view ) {
		this.particleModel = particleModel;
		this.particles = particleModel.particles;

		this.view = view;
	
		this.setSize( view.frameWidth, view.frameHeight, view.skew );
	
		this.k1ForcesX = new Array();
		this.k2ForcesX = new Array();
		this.k3ForcesX = new Array();
		this.k4ForcesX = new Array();
	
		this.k1ForcesY = new Array();
		this.k2ForcesY = new Array();
		this.k3ForcesY = new Array();
		this.k4ForcesY = new Array();
	
		this.k1VelocitiesX = new Array();
		this.k2VelocitiesX = new Array();
		this.k3VelocitiesX = new Array();
		this.k4VelocitiesX = new Array();
	
		this.k1VelocitiesY = new Array();
		this.k2VelocitiesY = new Array();
		this.k3VelocitiesY = new Array();
		this.k4VelocitiesY = new Array();		
	},

	/*
	 * Set boundaries.
	 * 
	 * @param {Object} frameWidth
	 * @param {Object} frameHeight
	 */
	setSize: function( frameWidth, frameHeight, skew ) {
		this.boundsLeft = (-frameWidth/2)/skew;
		this.boundsRight = (frameWidth/2)/skew;
		this.boundsTop = -frameHeight/2;
		this.boundsBottom = frameHeight/2;
	},

	/*
	 * Set up storage for a new particle.
	 * 
	 * @param {Number} i
	 */	
	allocateParticle: function( i ) {
		this.k1ForcesX[i] = 0;
		this.k2ForcesX[i] = 0;
		this.k3ForcesX[i] = 0;
		this.k4ForcesX[i] = 0;

		this.k1ForcesY[i] = 0;
		this.k2ForcesY[i] = 0;
		this.k3ForcesY[i] = 0;
		this.k4ForcesY[i] = 0;

		this.k1VelocitiesX[i] = 0;
		this.k2VelocitiesX[i] = 0;
		this.k3VelocitiesX[i] = 0;
		this.k4VelocitiesX[i] = 0;

		this.k1VelocitiesY[i] = 0;
		this.k2VelocitiesY[i] = 0;
		this.k3VelocitiesY[i] = 0;
		this.k4VelocitiesY[i] = 0;
	},

	/*
	 * Perform integration over x=1.
	 */	
	step: function() {
		var particles = this.particles;
		
		var k1ForcesX = this.k1ForcesX;
		var k2ForcesX = this.k2ForcesX;
		var k3ForcesX = this.k3ForcesX;
		var k4ForcesX = this.k4ForcesX;

		var k1ForcesY = this.k1ForcesY;
		var k2ForcesY = this.k2ForcesY;
		var k3ForcesY = this.k3ForcesY;
		var k4ForcesY = this.k4ForcesY;

		var k1VelocitiesX = this.k1VelocitiesX;
		var k2VelocitiesX = this.k2VelocitiesX;
		var k3VelocitiesX = this.k3VelocitiesX;
		var k4VelocitiesX = this.k4VelocitiesX;

		var k1VelocitiesY = this.k1VelocitiesY;
		var k2VelocitiesY = this.k2VelocitiesY;
		var k3VelocitiesY = this.k3VelocitiesY;
		var k4VelocitiesY = this.k4VelocitiesY;

		for (var i=0, l=particles.length; i<l; i++) {
			if (!particles[i].fixed && !particles[i].selected) {
				particles[i].originalPositionX = particles[i].positionX;
				particles[i].originalPositionY = particles[i].positionY;

				particles[i].originalVelocityX = particles[i].velocityX/2;
				particles[i].originalVelocityY = particles[i].velocityY/2;
			}
		}

		this.particleModel.applyForces();
		
		for (var i=0, l=particles.length; i<l; i++) {
			if (!particles[i].fixed && !particles[i].selected) {
				k1ForcesX[i] = particles[i].forceX;
				k1ForcesY[i] = particles[i].forceY;

				k1VelocitiesX[i] = particles[i].velocityX;
				k1VelocitiesY[i] = particles[i].velocityY;
			}
		}

		for (var i=0, l=particles.length; i<l; i++) {
			if (!particles[i].fixed && !particles[i].selected) {
				particles[i].positionX = particles[i].originalPositionX + k1VelocitiesX[i] * 0.5;
				particles[i].positionY = particles[i].originalPositionY + k1VelocitiesY[i] * 0.5;

				particles[i].velocityX = particles[i].originalVelocityX + (k1ForcesX[i] * 0.5)/particles[i].mass;
				particles[i].velocityY = particles[i].originalVelocityY + (k1ForcesY[i] * 0.5)/particles[i].mass;
			}
		}

		this.particleModel.applyForces();
		
		for (var i=0, l=particles.length; i<l; i++) {
			if (!particles[i].fixed && !particles[i].selected) {
				k2ForcesX[i] = particles[i].forceX;
				k2ForcesY[i] = particles[i].forceY;

				k2VelocitiesX[i] = particles[i].velocityX;
				k2VelocitiesY[i] = particles[i].velocityY;
			}
		}

		for (var i=0, l=particles.length; i<l; i++) {
			if (!particles[i].fixed && !particles[i].selected) {
				particles[i].positionX = particles[i].originalPositionX + k2VelocitiesX[i] * 0.5;
				particles[i].positionY = particles[i].originalPositionY + k2VelocitiesY[i] * 0.5;

				particles[i].velocityX = particles[i].originalVelocityX + (k2ForcesX[i] * 0.5)/particles[i].mass;
				particles[i].velocityY = particles[i].originalVelocityY + (k2ForcesY[i] * 0.5)/particles[i].mass;
			}
		}

		this.particleModel.applyForces();

		for (var i=0, l=particles.length; i<l; i++) {
			if (!particles[i].fixed && !particles[i].selected) {
				k3ForcesX[i] = particles[i].forceX;
				k3ForcesY[i] = particles[i].forceY;

				k3VelocitiesX[i] = particles[i].velocityX;
				k3VelocitiesY[i] = particles[i].velocityY;
			}
		}

		for (var i=0, l=particles.length; i<l; i++) {
			if (!particles[i].fixed && !particles[i].selected) {
				particles[i].positionX = particles[i].originalPositionX + k3VelocitiesX[i];
				particles[i].positionY = particles[i].originalPositionY + k3VelocitiesY[i];

				particles[i].velocityX = particles[i].originalVelocityX + (k3ForcesX[i])/particles[i].mass;
				particles[i].velocityY = particles[i].originalVelocityY + (k3ForcesY[i])/particles[i].mass;
			}
		}

		this.particleModel.applyForces();

		for (var i=0, l=particles.length; i<l; i++) {
			if (!particles[i].fixed && !particles[i].selected) {
				k4ForcesX[i] = particles[i].forceX;
				k4ForcesY[i] = particles[i].forceY;

				k4VelocitiesX[i] = particles[i].velocityX;
				k4VelocitiesY[i] = particles[i].velocityY;
			}
		}

		for (var i=0, l=particles.length; i<l; i++) {
			if (!particles[i].fixed && !particles[i].selected) {

				particles[i].positionX = particles[i].originalPositionX
					+ (1 / 6)
					* (k1VelocitiesX[i] + 2 * k2VelocitiesX[i] + 2
					* k3VelocitiesX[i] + k4VelocitiesX[i]);
				particles[i].positionY = particles[i].originalPositionY
					+ (1 / 6)
					* (k1VelocitiesY[i] + 2 * k2VelocitiesY[i] + 2
					* k3VelocitiesY[i] + k4VelocitiesY[i]);
/*
				if ( particles[i].positionX < this.boundsLeft+(this.view.nodes[particles[i].id].centerX) ) {
					particles[i].positionX = this.boundsLeft+(this.view.nodes[particles[i].id].centerX);
				} else if ( particles[i].positionX > (this.boundsRight-(particles[i].width/2)) ) {
					particles[i].positionX = this.boundsRight-(particles[i].width/2);
				}

				if ( particles[i].positionY < this.boundsTop+(this.view.nodes[particles[i].id].centerY) ) {
					particles[i].positionY = this.boundsTop+(this.view.nodes[particles[i].id].centerY);
				} else if ( particles[i].positionY > (this.boundsBottom-(this.view.nodes[particles[i].id].centerY)) ) {
					particles[i].positionY = this.boundsBottom-(this.view.nodes[particles[i].id].centerY);
				}
*/
				particles[i].velocityX = particles[i].originalVelocityX
					+ (1 / (6 * particles[i].mass))
					* (k1ForcesX[i] + 2 * k2ForcesX[i] + 2 * k3ForcesX[i] + k4ForcesX[i]);
				particles[i].velocityY = particles[i].originalVelocityY
					+ (1 / (6 * particles[i].mass))
					* (k1ForcesY[i] + 2 * k2ForcesY[i] + 2 * k3ForcesY[i] + k4ForcesY[i]);
			}
		}
	}
}

/* Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *     http://www.apache.org/licenses/LICENSE-2.0
 *     
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * 
 * Author: Kyle Scholz      http://kylescholz.com/
 * Copyright: 2006-2007
 */

/**
 * DataGraph
 * 
 * @author Kyle Scholz
 * 
 * @version 0.3
 * 
 * A DataGraph defines nodes and edges (relationships between nodes). It's a
 * data model from which all nodes or a subset of nodes may be included in a
 * particle model / view.
 * 
 * A DataGraph will notify subscribers when new nodes and edges have been added.
 * A subscriber must implement "newDataGraphNode" and "newDataGraphEdge".
 */
var DataGraph = function() {
	this.nodes = new Array();	
	this.subscribers = new Array();	
}

/*
 * Subscribe an observer
 * 
 * @param {DataGraphObserver} observer
 */
DataGraph.prototype.subscribe = function( observer ) {
	this.subscribers.push( observer );
}
	
/*
 * Notify subscribers of our new node
 * 
 * @param {DataGraphNode} node
 */
DataGraph.prototype.notifyNode = function( node ) {
	for( var i=0, l=this.subscribers.length; i<l; i++ ) {
		this.subscribers[i].newDataGraphNode( node );
	}
}
	
/*
 * Notify subscribers our node has left
 * 
 * @param {DataGraphNode} node
 * @author collin.miller@scribestorm.com
 */
DataGraph.prototype.unNotifyNode = function( node ) {
	for( var i=0, l=this.subscribers.length; i<l; i++ ) {
		this.subscribers[i].removeNode( node );
	}
}

/*
 * Notify subscribers of our new edge
 * 
 * @param {DataGraphNode} nodeA
 * @param {DataGraphNode} nodeB
 */
DataGraph.prototype.notifyEdge = function( nodeA, nodeB ) {
	for( var i=0, l=this.subscribers.length; i<l; i++ ) {
		this.subscribers[i].newDataGraphEdge( nodeA, nodeB );
	}
}

/*
 * Add node to the DataGraph.
 * 
 * @param {DataGraphNode} dataGraphNode
 */
DataGraph.prototype.addNode = function( dataGraphNode ) {
	dataGraphNode.id = this.nodes.length;
	dataGraphNode.rendered = false;
	this.nodes.push( dataGraphNode );		
	this.notifyNode( dataGraphNode );
}

/*
 * Remove node from the DataGraph.
 * 
 * @param {DataGraphNode} dataGraphNode
 * @author collin.miller@scribestorm.com
 */
DataGraph.prototype.removeNode = function( dataGraphNode ) {
	dataGraphNode.id = this.nodes.indexOf(dataGraphNode);
	for(var i=1; i<this.nodes.length; i++) {
		if (this.nodes[i] == dataGraphNode) {
			delete this.nodes[i];
		}
	}		
	this.unNotifyNode( dataGraphNode );
}
	
/*
 * Add an edge between two nodes.
 * 
 * @param {DataGraphNode} nodeA
 * @param {DataGraphNode} nodeB
 */
DataGraph.prototype.addEdge = function( nodeA, nodeB ) {
	var success = nodeA.addEdge( nodeB, 1 );
	if ( success ) {
		this.notifyEdge( nodeA, nodeB );			
	}
}

/**
 * A DataGraphNode is a node in a DataGraph (...duh)
 * 
 * @param fixed indicates whether the node has a fixed position
 */
var DataGraphNode = function() {
	this.edges = {};
	this.edgeCount = 0;
}

/*
 * Add an edge between two nodes.
 * 
 * @param {DataGraphNode} node
 */
DataGraphNode.prototype.addEdge = function( node ) {
	if ( !(node.id in this.edges) && !(this.id in node.edges) && (this.id != node.id) ) {
		this.edgeCount++;
		this.edges[node.id] = node;
		return true;
	}
	return false;
}

/* Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *     http://www.apache.org/licenses/LICENSE-2.0
 *     
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * 
 * Author: Kyle Scholz      http://kylescholz.com/
 * Copyright: 2006-2007
 */

/**
 * ForceDirectedLayout
 *  
 * @author Kyle Scholz
 * 
 * @version 0.3.3
 * 
 * @param {DOMElement} container
 */
var ForceDirectedLayout = function( container, useVectorGraphics ) {
	
	this.container = container;
	this.containerLeft=0; this.containerTop=0;
	this.containerWidth=0; this.containerHeight=0;

	this.svg = useVectorGraphics && document.implementation.hasFeature("org.w3c.dom.svg", '1.1') ? true : false; 

	// Render model with SVG if it's supported.
	if ( this.svg ) {
		this.view = new SVGGraphView( container, 1 );
	// Otherwise, use HTML.
	} else {
		this.view = new HTMLGraphView( container, 1 );
	}

	// Create the model that we'll use to represent the nodes and relationships 
	// in our graph.
	this.model = new ParticleModel( this.view );
	this.model.start();

	this.setSize();
	
	// for queueing loaders
	this.dataNodeQueue = new Array();
	this.relationshipQueue = new Array();

	// the data graph defines the nodes and edges
	this.dataGraph = new DataGraph();
	this.dataGraph.subscribe( this );
				
	// if this is IE, turn on caching of javascript-loaded images explicitly
	if ( document.all ) {
		document.createStyleSheet().addRule('html', 
			'filter:expression(document.execCommand("BackgroundImageCache", false, true))' );		
	}

	// attach an onresize event
	var resizeEvent = new EventHandler( this, this.setSize );
	if (window.addEventListener) {
		window.addEventListener("resize",resizeEvent,false);
	} else {
		window.attachEvent("onresize",resizeEvent);
	}

	// attach an onmousemove event
	if (window.Event) { document.captureEvents(Event.MOUSEMOVE); }
	var mouseMoveEvent = new EventHandler( this, this.handleMouseMoveEvent );
	if (document.addEventListener) {
		document.addEventListener("mousemove",mouseMoveEvent,false);
	} else {
		document.attachEvent("onmousemove",mouseMoveEvent);
	}

	// attach an onmouseup event
	var mouseUpEvent = new EventHandler( this, this.handleMouseUpEvent );
	if (document.addEventListener) {
		document.addEventListener("mouseup",mouseUpEvent,false);
	} else {
		document.attachEvent("onmouseup",mouseUpEvent);
	}
}

/*
 * Respond to a resize event in the browser.
 */
ForceDirectedLayout.prototype.setSize = function() {
	if ( this.container.tagName == "BODY" ) {
		// Get the size of our window. 
		if (document.all) {
			this.containerWidth = document.body.offsetWidth - 5;
	      	this.containerHeight = document.documentElement.offsetHeight - 5;
		} else {
			this.containerWidth = window.innerWidth - 5;
			this.containerHeight = window.innerHeight - 5;
		}
		this.containerLeft = 0;
		this.containerTop = 0;
	} else {
		this.containerWidth = this.container.offsetWidth;
		this.containerHeight = this.container.offsetHeight;

		this.containerLeft = this.container.offsetLeft;
		this.containerTop = this.container.offsetTop;
	}
	this.view.setSize( this.containerLeft, this.containerTop,
		this.containerWidth, this.containerHeight );
	this.model.setSize( this.containerWidth, this.containerHeight );
	
	this.model.draw( true );
}

/*
 * A default mousemove handler. Moves the selected node and updates child
 * positions according to geometric model.
 * 
 * @param {Object} e
 */
ForceDirectedLayout.prototype.handleMouseMoveEvent = function( e ) {
	if ( this.model.selected && !this.model.particles[this.model.selected].fixed ) {

		// TODO: This is a very temporary fix. In Firefox 2, our EventHandler
		// factory piles mouse events onto the arguments list.
		e = arguments[arguments.length-1];			
		var mouseX = e.pageX ? e.pageX : e.clientX;
		var mouseY = e.pageY ? e.pageY : e.clientY;

		mouseX -= this.view.centerX;
		mouseY -= this.view.centerY;

		// set the node position
		this.model.particles[this.model.selected].positionX=mouseX/this.view.skewX;
		this.model.particles[this.model.selected].positionY=mouseY/this.view.skewY;
		this.model.tick();
    }
}

/*
 * A default mouseup handler. Resets the selected node's position
 * and clears the selection.
 */	
ForceDirectedLayout.prototype.handleMouseUpEvent = function() {
	if ( this.model.selected ) {
		this.model.particles[this.model.selected].selected = false;
		this.model.reset();
		this.model.selected = null;
	}
}

/*
 * A default mousedown handler. Sets the selected node.
 * 
 * @param {Number} id
 */
ForceDirectedLayout.prototype.handleMouseDownEvent = function( id ) {
	this.model.selected = id;
	this.model.particles[id].selected = true;

}

/*
 * Handle a new node.
 *  
 * @param {DataGraphNode} dataNode
 */
ForceDirectedLayout.prototype.newDataGraphNode = function( dataNode ) {
	this.enqueueNode( dataNode );						
}

ForceDirectedLayout.prototype.removeNode = function( dataNode ) {
	this.removeParticle(dataNode);
}

ForceDirectedLayout.prototype.newDataGraphEdge = function( nodeA, nodeB ) {
	this.enqueueRelationship( nodeA, nodeB );
}

/*
 * Enqueue a node for modeling later.
 * 
 * @param {DataGraphNode} dataNode
 */
ForceDirectedLayout.prototype.enqueueNode = function( dataNode ) {
	this.dataNodeQueue.push( dataNode );
}

/*
 * Dequeue a node and create a particle representation in the model.
 * 
 * @param {DataGraphNode} dataNode
 */
ForceDirectedLayout.prototype.dequeueNode = function() {
	var node = this.dataNodeQueue.shift();
	if ( node ) {
		this.addParticle( node );
		return true;						
	}
	return false;
}

/*
 * Enqueue a relationship for modeling later.
 * 
 * @param {DataGraphNode} nodeA
 * @param {DataGraphNode} nodeB
 */
ForceDirectedLayout.prototype.enqueueRelationship = function( nodeA, nodeB ) {
	this.relationshipQueue.push( {'nodeA': nodeA, 'nodeB': nodeB} );
}

/*
 * Dequeue a node and create a particle representation in the model.
 */
ForceDirectedLayout.prototype.dequeueNode = function() {
	var node = this.dataNodeQueue.shift();
	if ( node ) {
		this.addParticle( node );
		return true;					
	}
	return false;
}

/*
 * Dequeue a relationship and add to the model.
 */
ForceDirectedLayout.prototype.dequeueRelationship = function() {
	var edge = this.relationshipQueue[0]
	if ( edge && edge.nodeA.particle && edge.nodeB.particle ) {
		this.relationshipQueue.shift();
		this.addSimilarity( edge.nodeA, edge.nodeB );						
	}	
}

/*
 * Called by timer to control dequeuing of nodes into addNode.
 */
ForceDirectedLayout.prototype.update = function() {
	this.dequeueNode();
	this.dequeueRelationship();
}

/*
 * Clear all nodes and edges connected to the root.
 */
ForceDirectedLayout.prototype.clear = function( modelNode ) {
	this.model.clear();
}

/*
 * Recenter the graph on the specified node.
 */
ForceDirectedLayout.prototype.recenter = function( modelNode ) {
// todo
}

/*
 * Create a default configuration object with a reference to our layout.
 * 
 * @param {Particle} layout
 */
ForceDirectedLayout.prototype.config = function( layout ) {
	// A default configuration class. This is used if a
	// className was not indicated in your dataNode or if the
	// indicated class was not found.
	this._default={
		model: function( dataNode ) {
			return {
				mass: 1
			};
		},
		view: function( dataNode, modelNode ) {
			return layout.defaultNodeView( dataNode, modelNode );
		}
	}
}

/*
 * Default forces configuration
 */
ForceDirectedLayout.prototype.forces={
	spring: {
		_default: function( nodeA, nodeB, isParentChild ) {
			if (isParentChild) {
				return {
					springConstant: 0.5,
					dampingConstant: 0.2,
					restLength: 20
				}
			} else {
				return {
					springConstant: 0.2,
					dampingConstant: 0.2,
					restLength: 20
				}
			}
		}
	},
	magnet: function() {
		return {
			magnetConstant: -2000,
			minimumDistance: 10
		}
	}
};

/*
 * Add a particle to the model and view.
 * 
 * @param {DataGraphNode} node
 */
ForceDirectedLayout.prototype.addParticle = function( dataNode ) {
	// Create a particle to represent this data node in our model.
	var particle = this.makeNodeModel(dataNode);
	
	var domElement = this.makeNodeView( dataNode, particle );
	this.view.addNode( particle, domElement );

	// Determine if this particle's position should be fixed.
	if ( dataNode.fixed ) { particle.fixed = true; }

	// Assign a random position to the particle.
	var rx = Math.random()*2-1;
	var ry = Math.random()*2-1;
	particle.positionX = rx;
	particle.positionY = ry;

	// Add a Spring Force between child and parent
	if ( dataNode.parent ) {
		particle.positionX = dataNode.parent.particle.positionX + rx;
		particle.positionY = dataNode.parent.particle.positionY + ry;
		var configNode = (dataNode.type in this.forces.spring &&
			dataNode.parent.type in this.forces.spring[dataNode.type]) ? 
			this.forces.spring[dataNode.type][dataNode.parent.type](dataNode, dataNode.parent, true) : 
			this.forces.spring['_default'](dataNode, dataNode.parent, true);
		this.model.makeSpring( particle, dataNode.parent.particle, 
			configNode.springConstant, configNode.dampingConstant, configNode.restLength );

		var props = this.viewEdgeBuilder( dataNode.parent, dataNode );
		this.view.addEdge( particle, dataNode.parent.particle, props );
	}

	// Add repulsive force between this particle and all other particle.
	for( var j=0, l=this.model.particles.length; j<l; j++ ) {
		if ( this.model.particles[j] != particle ) {
			var magnetConstant = this.forces.magnet()['magnetConstant'];
			var minimumDistance = this.forces.magnet()['minimumDistance'];
			this.model.makeMagnet( particle, this.model.particles[j], magnetConstant, minimumDistance );
		}
	}

	dataNode.particle = particle;
	dataNode.viewNode = domElement;
	return dataNode;
}

/*
 * Remove a particle from the model and the view.
 * 
 * @param {DataGraphNode} node
 */

ForceDirectedLayout.prototype.removeParticle = function(dataNode) {
	this.view.removeNode(dataNode.particle);
}

/*
 * Add a spring force between two edges + corresponding edge in the view.
 * 
 * @param {Number} springConstant
 * @param {DataGraphNode} nodeA
 * @param {DataGraphNode} nodeB
 */
ForceDirectedLayout.prototype.addSimilarity = function( nodeA, nodeB ) {
	var configNode = (nodeA.type in this.forces.spring &&
		nodeB.type in this.forces.spring[nodeA.type]) ? 
		this.forces.spring[nodeA.type][nodeB.parent.type](nodeA,nodeB,false) : 
		this.forces.spring['_default'](nodeA,nodeB,false);

	this.model.makeSpring( nodeA.particle, nodeB.particle,
		configNode.springConstant, configNode.dampingConstant, configNode.restLength );

	var props = this.viewEdgeBuilder( nodeA, nodeB );
	this.view.addEdge( nodeA.particle, nodeB.particle, props );
}

/* Build node views from configuration
 * 
 * @param {DataGraphNode} dataNode
 * @param {SnowflakeNode} modelNode
 */
ForceDirectedLayout.prototype.makeNodeView = function( dataNode, modelNode ) {
	var configNode = (dataNode.type in this.config) ? this.config[dataNode.type] : this.config['_default'];
	return configNode.view( dataNode, modelNode );					
}

/* Build model nodes from configuration
 * 
 * @param {DataGraphNode} dataNode
 */
ForceDirectedLayout.prototype.makeNodeModel = function( dataNode ) {
	var configNode = (dataNode.type in this.config) ? this.config[dataNode.type] : this.config['_default']; 
	for( var attribute in configNode.model(dataNode) ) {
		dataNode[attribute] = configNode.model(dataNode)[attribute];
	}
	var modelNode = this.model.makeParticle( dataNode.mass, 0, 0 );
	return modelNode;
}

/* Default node view builder
 * 
 * @param {SnowflakeNode} modelNode
 * @param {DataNode} dataNode
 */
ForceDirectedLayout.prototype.defaultNodeView = function( dataNode, modelNode ) {
	var nodeElement;
	if ( this.svg ) {
		nodeElement = document.createElementNS("http://www.w3.org/2000/svg", "circle");
		nodeElement.setAttribute('stroke', '#444444');
		nodeElement.setAttribute('stroke-width', '.25px');
		nodeElement.setAttribute('fill', "#aaaaaa");
		nodeElement.setAttribute('r', 6 + 'px');
		nodeElement.onmousedown =  new EventHandler( this, this.handleMouseDownEvent, modelNode.id )
	} else {
		nodeElement = document.createElement( 'div' );
		nodeElement.style.position = "absolute";
		nodeElement.style.width = "12px";
		nodeElement.style.height = "12px";
		nodeElement.style.backgroundImage = "url(http://kylescholz.com/cgi-bin/bubble.pl?title=&r=12&pt=8&b=444444&c=aaaaaa)";
		nodeElement.innerHTML = '<img width="1" height="1">';
		nodeElement.onmousedown =  new EventHandler( this, this.handleMouseDownEvent, modelNode.id )
	}
	return nodeElement;
}

/* Default edge view builder 
 * 
 * @param {DataNode} dataNodeSrc
 * @param {DataNode} dataNodeDest
 */
ForceDirectedLayout.prototype.makeEdgeView = function( dataNodeSrc, dataNodeDest ) {
	var props;
	if ( this.svg ) {
		props = {
			'stroke': '#888888',
			'stroke-width': '2px',
			'stroke-dasharray': '2,4'
		}
	} else {
		props = {
			'pixelColor': '#888888',
			'pixelWidth': '2px',
			'pixelHeight': '2px',
			'pixels': 5
		}
	}
	return props;
}

ForceDirectedLayout.prototype.viewEdgeBuilder = ForceDirectedLayout.prototype.makeEdgeView;

/* Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *     http://www.apache.org/licenses/LICENSE-2.0
 *     
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * 
 * Author: Kyle Scholz      http://kylescholz.com/
 * Copyright: 2006-2007
 */

/**
 * HTMLGraphView
 * 
 * @author Kyle Scholz
 * 
 * @version 0.3.3
 * 
 * Represents a view on a GraphModel. This implementation supports HTML elements.
 * 
 * @param {HTMLElement} container
 * @param {Boolean} skewView (optional) Indicates whether we should draw on a 'skewed' canvas.
 */
var HTMLGraphView = function( container, skewView ) {

	this.container = container;

	this.frameLeft = 0;
	this.frameTop = 0;

	this.skewView = skewView;
	this.skewBase = 0;
	this.skewX = 1;
	this.skewY = 1;

	this['nodes'] = {};
	this['edges'] = {};

	this.defaultEdgeProperties = {
		'pixelColor': '#c4c4c4',
		'pixelWidth': '2px',
		'pixelHeight': '2px',
		'pixels': 4
	}
}

/*
 * @param {Number} frameLeft
 * @param {Number} frameTop
 * @param {Number} frameWidth
 * @param {Number} frameHeight
 */
HTMLGraphView.prototype.setSize = function( frameLeft, frameTop, frameWidth, frameHeight ) {
	this.frameLeft = frameLeft;
	this.frameTop = frameTop;
	this.frameWidth = frameWidth;
	this.frameHeight = frameHeight;

	this.centerX = parseInt(frameWidth/2);
	this.centerY = parseInt(frameHeight/2);		

	if ( this.skewView && this.skewBase) {
		this.skewX = this.frameWidth/this.skewBase;
		this.skewY = this.frameHeight/this.skewBase;
	} else {
		this.skewX = 1;
		this.skewY = 1;
	}
}

/*
 * Add a node to the view. 
 *
 * @param {Particle} particle
 * @param {DOMNode} domElement
 * @param {Number} centerOffsetX, Position of center of domNode relative to 
 * 		left. If not provided, SVG elements are assumed centered. The center of
 * 		HTML elements is set to offsetWidth/2.
 * @param {Number} centerOffsetY, Position of center of domNode relative to 
 * 		top. If not provided, SVG elements are assumed centered. The center of
 * 		HTML elements is determined by offsetHeight/2.
 */
HTMLGraphView.prototype.addNode = function( particle, domElement,
	centerOffsetX, centerOffsetY ) {

	this.container.appendChild(domElement);
	domElement.style.zIndex=10;
	if ( centerOffsetX == null ) {
		centerOffsetX = parseInt( domElement.offsetWidth/2 );
	}
	if ( centerOffsetY == null ) {
		centerOffsetY = parseInt( domElement.offsetHeight/2 );
	}

	this.nodes[particle.id] = {
		domElement: domElement,
		centerX: centerOffsetX,
		centerY: centerOffsetY			
	}
	domElement.style.left="0px";
	domElement.style.top="0px";

	this.drawNode(particle);
	return domElement;
}

/*
 * Drop node, eliminating dom element from document
 */
HTMLGraphView.prototype.removeNode = function( particle ) {
	if ( particle ) {
		var domElement = this.nodes[particle.id].domElement;
		this.container.removeChild( domElement );
	
		// delete edges to particle
		for ( var e in this.edges[particle.id] ) {
			this.removeEdge(this.edges[particle.id][e]);
		}
		
		// delete edges to particle
		for ( var e in this.edges ) {
			if ( this.edges[e][particle.id] ) {
				this.removeEdge(this.edges[e][particle.id]);
			}
		}
	
		delete this.nodes[particle.id];
	}
}

/*
 * Add an edge to the view.
 * 
 * @param {Particle} particleA
 * @param {Particle} particleB
 */
HTMLGraphView.prototype.addEdge = function( particleA, particleB, edgeProperties ) {

	if ( !this['edges'][particleA.id] ) {
		this['edges'][particleA.id]={};
	}

	if ( !this['edges'][particleA.id][particleB.id] ) {		
		// create the "pixels" used to draw the edge
		var edgePixels = new Array();

		if ( !edgeProperties ) {
			edgeProperties = this.defaultEdgeProperties;
		}
	
		var pixelCount = edgeProperties.pixels;
		var pixels = [];

		for ( var k=0, l=pixelCount; k<l; k++ ) {
			var pixel = document.createElement('div');
			pixel.style.width = edgeProperties.pixelWidth;
			pixel.style.height = edgeProperties.pixelHeight;
			pixel.style.backgroundColor = edgeProperties.pixelColor;
			pixel.style.position = 'absolute';
			pixel.innerHTML="<img height=1 width=1/>";
			edgePixels.push( pixel );
			this.container.appendChild(pixel);
		}

		this['edges'][particleA.id][particleB.id] = {
			source: particleA,
			target: particleB,
			edge: edgePixels
		}
		return edgePixels;
	} else {
		return this['edges'][particleA.id][particleB.id].edge;
	}
}

/*
 * Drop edge, eliminating dom element from document
 */
HTMLGraphView.prototype.removeEdge = function( edge ) {
	var domElement = edge.domEdge;
	var particleA = edge.source;
	var particleB = edge.target;
	for( var i=0; i<edge.edge.length; i++ ) {
		this.container.removeChild(edge.edge[i]);		
	}
	delete this['edges'][particleA.id][particleB.id];
}

/*
 * Draw a node at it's current position.
 * 
 * @param {Particle} particle
 */
HTMLGraphView.prototype.drawNode = function( particle ) {
	var domNodeProps = this['nodes'][particle.id];
	if ( domNodeProps ) {
		var domNode = domNodeProps.domElement;

		domNode.style.left = (particle.positionX*this.skewX) - 
			domNodeProps.centerX + this.centerX + 'px';
		domNode.style.top = particle.positionY*this.skewY - 
			domNodeProps.centerY + this.centerY + 'px';

		var e = this.edges[particle.id];
		for ( var t in e ) {
			this.drawEdge( particle, e[t]['target'] );
		}
	}
}

/*
 * Draw an edge at it's current position.
 * 
 * @param {Particle} particleA
 * @param {Particle} particleB
 */
HTMLGraphView.prototype.drawEdge = function ( nodeI, nodeJ ) {
	// get a distance vector between nodes
	var dx = nodeI.positionX - nodeJ.positionX;
	var dy = nodeI.positionY - nodeJ.positionY;
	if (dx == 0 && dy == 0) return;

	var distance = Math.sqrt( dx*dx	+ dy*dy );
		
	var pixels = this['edges'][nodeI.id][nodeJ.id]['edge'];

	// draw a line between particles using the "pixels"
	for ( var k=0, l=pixels.length; k<l; k++ ) {
		var p = (distance / l) * k;
		pixels[k].style.left=parseInt(nodeI.positionX +(-1)*p*(dx/distance))*this.skewX + this.centerX + 'px';
		pixels[k].style.top=parseInt(nodeI.positionY +(-1)*p*(dy/distance))*this.skewY + this.centerY + 'px';
	}
}
	
/*
 * Remove everything from the view.
 */
HTMLGraphView.prototype.clear = function() {
	// first, remove all the edges
	for ( var e in this.edges ) {
		for ( var eb in this.edges[e] ) {
		// get the pixels that make up the edge
			for( var i=0, l=this.edges[e][eb].edge.length; i<l; i++ ) {
				this.container.removeChild( this.edges[e][eb].edge[i] );
			}
		}
	}
		
	this.edges = {};

	// now remove the nodes
	for ( var n in this.nodes ) {
		this.container.removeChild( this.nodes[n].domElement );
	}
		
	this.nodes = {};
}

/* Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *     http://www.apache.org/licenses/LICENSE-2.0
 *     
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * 
 * Author: Kyle Scholz      http://kylescholz.com/
 * Author: Ted Mielczarek	http://ted.mielczarek.org/
 * Copyright: 2006-2007
 */

/**
 * SVGGraphView
 * 
 * @author Kyle Scholz
 * @author Ted Mielczarek
 * 
 * @version 0.3.3
 * 
 * Represents a view on a GraphModel. This implementation supports SVG
 * elements as well as HTML elements.
 * 
 * Since SVG isn't universally supported, I suggest you offer HTMLGraphView
 * to less-evolved browsers. Try this to assign the appropriate view:
 * 
 * var view;
 * if ( document.implementation.hasFeature("org.w3c.dom.svg", '1.1') ) {
 *     view=new SVGGraphView();
 * } else {
 *     view=new HTMLGraphView();
 * }
 * 
 * @param {HTMLElement} container
 * @param {Boolean} skewView (optional) Indicates whether we should draw on a 'skewed' canvas.
 */
var SVGGraphView = function( container, skewView ) {

	this.container = container;

	this.frameLeft = 0;
	this.frameTop = 0;

	this.skewView = skewView;
	this.skewBase = 0;
	this.skewX = 1;
	this.skewY = 1;

	this['nodes'] = {};

	this['edges'] = {};
		
	this.svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");

	this.svg.setAttribute("version", "1.1");
	this.container.appendChild( this.svg );

	this.eg = document.createElementNS("http://www.w3.org/2000/svg", "g");
	this.svg.appendChild(this.eg);

	this.ng = document.createElementNS("http://www.w3.org/2000/svg", "g");
   	this.svg.appendChild(this.ng);

	this.defaultEdgeProperties = {
		'stroke': '#c4c4c4',
		'stroke-width': '2px',
		'stroke-dasharray': '2,8'			
	}
}

/*
 * @param {Number} frameLeft
 * @param {Number} frameTop
 * @param {Number} frameWidth
 * @param {Number} frameHeight
 */
SVGGraphView.prototype.setSize = function( frameLeft, frameTop, frameWidth, frameHeight ) {
	this.frameLeft = frameLeft;
	this.frameTop = frameTop;
	this.frameWidth = frameWidth;
	this.frameHeight = frameHeight;

	this.centerX = parseInt(frameWidth/2);
	this.centerY = parseInt(frameHeight/2);		

	if ( this.skewView && this.skewBase) {
		this.skewX = this.frameWidth/this.skewBase;
		this.skewY = this.frameHeight/this.skewBase;
	} else {
		this.skewX = 1;
		this.skewY = 1;
	}

   	this.svg.setAttribute("width", this.frameWidth);
   	this.svg.setAttribute("height", this.frameHeight);
	var dimString = parseInt(-1*this.frameWidth/2) + " " + parseInt(-1*this.frameHeight/2)
		+ " " + this.frameWidth + " " + this.frameHeight;
	this.svg.setAttribute("viewBox", dimString);
}

/*
 * Add a node to the view. 
 *
 * @param {Particle} particle
 * @param {DOMNode} domElement
 * @param {Number} centerOffsetX, Position of center of domNode relative to 
 * 		left. If not provided, SVG elements are assumed centered. The center of
 * 		HTML elements is set to offsetWidth/2.
 * @param {Number} centerOffsetY, Position of center of domNode relative to 
 * 		top. If not provided, SVG elements are assumed centered. The center of
 * 		HTML elements is determined by offsetHeight/2.
 */
SVGGraphView.prototype.addNode = function( particle, domElement,
	centerOffsetX, centerOffsetY ) {

	// With an SVG View Element
	if ( domElement.localName=="circle" || domElement.localName == "text" ) {
		this.ng.appendChild(domElement);
		centerOffsetX = 0;
		centerOffsetY = 0;

	// With an HTML View Element
	} else {
		this.container.appendChild(domElement);
		domElement.style.zIndex=10;
		if ( centerOffsetX == null ) {
			centerOffsetX = parseInt( domElement.offsetWidth/2 );
		}
		if ( centerOffsetY == null ) {
			centerOffsetY = parseInt( domElement.offsetHeight/2 );
		}
	}

	this.nodes[particle.id] = {
		domElement: domElement,
		centerX: centerOffsetX,
		centerY: centerOffsetY			
	}

	this.drawNode(particle);
	return domElement;
}

/*
 * Drop node, eliminating dom element from document
 */
SVGGraphView.prototype.removeNode = function( particle ) {
	if ( particle ) {
	var domElement = this.nodes[particle.id].domElement;
	if ( domElement.localName=="circle" || domElement.localName == "text" ) {
		this.ng.removeChild( domElement );
	} else {
		this.container.removeChild( domElement );
	}

	// delete edges to particle
	for ( var e in this.edges[particle.id] ) {
		this.removeEdge(this.edges[particle.id][e]);
	}
	
	// delete edges to particle
	for ( var e in this.edges ) {
		if ( this.edges[e][particle.id] ) {
			this.removeEdge(this.edges[e][particle.id]);
		}
	}

	delete this.nodes[particle.id];
	}
}

/*
 * Add an Edge to the view.
 * 
 * @param {Particle} particleA
 * @param {Particle} particleB
 * @param {Object} edgeProperties
 */
SVGGraphView.prototype.addEdge = function( particleA, particleB, edgeProperties ) {
	if ( !this['edges'][particleA.id] ) {
		this['edges'][particleA.id]={};
	}

	if ( !this['edges'][particleA.id][particleB.id] ) {
		var edge = document.createElementNS("http://www.w3.org/2000/svg", "polyline");
		if ( !edgeProperties ) {
			edgeProperties = this.defaultEdgeProperties;
		}
		for ( var p in edgeProperties ) {
			edge.setAttribute( p, edgeProperties[p] );
		}

		this.edges[particleA.id][particleB.id] = edge;
		edge.id = 'edge'+particleA.id+':'+particleB.id;
		this.eg.appendChild(edge);

		this['edges'][particleA.id][particleB.id] = {
			source: particleA,
			target: particleB,
			domEdge: edge
		}
		return edge;
	} else {
		return this['edges'][particleA.id][particleB.id].domEdge;
	}
}

/*
 * Drop edge, eliminating dom element from document
 */
SVGGraphView.prototype.removeEdge = function( edge ) {
	var domElement = edge.domEdge;
	var particleA = edge.source;
	var particleB = edge.target;
	this.eg.removeChild(domElement);
	delete this['edges'][particleA.id][particleB.id];
}

/*
 * Draw a node at it's current position.
 * 
 * @param {Particle} particle
 */
SVGGraphView.prototype.drawNode = function( particle ) {
	var domNodeProps = this['nodes'][particle.id];
	if ( domNodeProps ) {
		var domNode = domNodeProps.domElement;
		if( domNode.localName == 'circle' ) {
			domNode.setAttribute('transform','translate(' + particle.positionX*this.skewX + ' ' + particle.positionY*this.skewY + ')');
		} else if ( domNode.localName == 'text' ) {
			domNode.setAttribute('transform','translate(' + (particle.positionX*this.skewX - 
				domNode.getAttribute("width")) + ' ' + (particle.positionY*this.skewY - 
				domNode.getAttribute("height")) + ')');
		} else {
			domNode.style.left = (particle.positionX*this.skewX) - 
				domNodeProps.centerX + this.centerX + 'px';
			domNode.style.top = particle.positionY*this.skewY - 
				domNodeProps.centerY + this.centerY + 'px';
		}
	
		var e = this.edges[particle.id];
		for ( var t in e ) {
			this.drawEdge( particle, e[t]['target'] );
		}
	}
}

var Distance = function( x1, y1, x2, y2 ) {;
	this['dx'] = x1 - x2;
	this['dy'] = y1 - y2;
	this['d2'] = (this['dx']*this['dx']+this['dy']*this['dy']);
	this['d'] = Math.sqrt(this['d2']);
}

/*
 * Draw an edge at it's current position.
 * 
 * @param {Particle} particleA
 * @param {Particle} particleB
 */
SVGGraphView.prototype.drawEdge = function ( particleA, particleB ) {
	var edge = this.edges[particleA.id][particleB.id]['domEdge'];

	edge.setAttribute('points',
		(particleA.positionX)*this.skewX + "," + (particleA.positionY)*this.skewY + "," + 
		(particleB.positionX)*this.skewX + "," + (particleB.positionY)*this.skewY);
}
	
/*
 * Remove everything from the view.
 */
SVGGraphView.prototype.clear = function() {
	// first, remove all the edges
	for ( var e in this.edges ) {
		for ( var eb in this.edges[e] ) {
			this.eg.removeChild( this.edges[e][eb].domEdge );
		}
	}

	this.edges = {};

	// now remove the nodes
	for ( var n in this.nodes ) {
		var domElement = this.nodes[n].domElement;
		if (domElement.localName=="circle" || domElement.localName=="text") {
			this.ng.removeChild(domElement);
		} else {
//			document.body.removeChild(domElement);
			domElement.parentNode.removeChild(domElement);
		}
	}		

	this.nodes = {};
}

/* Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *     http://www.apache.org/licenses/LICENSE-2.0
 *     
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * 
 * Author: Kyle Scholz      http://kylescholz.com/
 * Copyright: 2006-2007
 */

/**
 * Timer
 * 
 * @author Kyle Scholz
 * 
 * @version 0.3
 * 
 * Timer with very rough throttle.
 * 
 * @author Kyle Scholz
 */
var Timer = function( timeout ) {
	this.init( timeout );
};
Timer.prototype = {

	/*
	 * Initialize the Timer with the indicated timeout.
	 * 
	 * @param {Object} timeout
	 */
	init: function( timeout ) {
		this['timer'];
		this['TIMEOUT'] = timeout;
		this['BASE_TIMEOUT'] = timeout;
		this['interupt'] = true;
		this['subscribers'] = new Array();
		this['ontimeout'] = new EventHandler( this,
			// notify subscribers and restart timer
			function() {
				this.notify();
				if ( !this.interupt ) { this.start(); }
			}
		);
	},

	/*
	 * Start the Timer.
	 */
	start: function() {
		this['interupt']=false;
		this['timer'] = window.setTimeout(this.ontimeout,this['TIMEOUT']);
	},

	/*
	 * Stop the Timer.
	 */
	stop: function() {
		this['interupt']=true;
	},

	/*
	 * Subscribe an observer.
	 */
	subscribe: function( observer ) {
		this.subscribers.push( observer );
	},

	/*
	 * Notify observers when a tick event has occured.
	 */
	notify: function() {
		for( var i=0; i<this.subscribers.length; i++ ) {
			var nextTimeout = this.subscribers[i].update();
			if ( nextTimeout == false ) {
				this.stop();
			} else if ( nextTimeout != null ) {
				this['TIMEOUT']=nextTimeout;
			}
		}
	}
}

/* Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *     http://www.apache.org/licenses/LICENSE-2.0
 *     
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * 
 * Author: Kyle Scholz      http://kylescholz.com/
 * Copyright: 2006-2007
 */

/**
 * EventHandler
 * 
 * @author Kyle Scholz
 * 
 * @version 0.3
 * 
 * A factory for producing event handlers w/ contextual scope.
 * 
 * @param {Object} _caller: an object with scope needed by handler
 * @param {Object} _handler: an event handler function
 * 
 * Any additional arguments will be passed to _handler. The source event will be
 * the last argument in the list if it's available.
 */
var EventHandler = function( _caller, _handler ) {
	var args=new Array();
	for( var i=2; i<arguments.length; i++ ) {
		args.push( arguments[i] );
	}
	return( function( e ) {
			if( e ) { args.push(e); }
			_handler.apply( _caller, args );
	} );
};